Skip to content

feat(frontend): support schema evolution in data branch diff/merge#25882

Open
VioletQwQ-0 wants to merge 44 commits into
matrixorigin:mainfrom
VioletQwQ-0:codex/takeover-24666-data-branch-schema-evolution
Open

feat(frontend): support schema evolution in data branch diff/merge#25882
VioletQwQ-0 wants to merge 44 commits into
matrixorigin:mainfrom
VioletQwQ-0:codex/takeover-24666-data-branch-schema-evolution

Conversation

@VioletQwQ-0

@VioletQwQ-0 VioletQwQ-0 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

  • API-change
  • BUG
  • Improvement
  • Documentation
  • Feature
  • Test and CI
  • Code Refactoring

Which issue(s) this PR fixes:

issue #24549

What this PR does / why we need it:

This PR continues the unfinished work from #24666 while preserving its original commits and authorship.

It supports schema evolution in DATA BRANCH DIFF/MERGE by matching columns with stable identity, separating physical and visible common-column indexes, projecting compatible historical batches, comparing common columns, preserving target-only data, and maintaining ALTER lineage ownership.

Review fixes on the latest head:

  • project reordered and target-only historical layouts without moving incompatible vectors;
  • reject same-name DROP/ADD column redefinitions by Seqnum, while preserving pure FIRST / AFTER reorderings;
  • retain ALTER snapshot/metadata pairs while table-, database-, or account-level historical owners still cover the component, including current-table-first deletion;
  • reclaim deleted intermediate ALTER generations after the final logical or historical owner disappears;
  • preserve explicit-transaction ALTER semantics for ordinary snapshot/PITR-covered tables.

Validation

  • go test ./pkg/frontend/databranchutils ./pkg/frontend ./pkg/sql/compile ./pkg/vm/engine/test -count=1
  • focused go test -race for identity mapping, projection, and lineage-owner lifecycle paths
  • go vet ./pkg/frontend/databranchutils ./pkg/frontend ./pkg/sql/compile ./pkg/vm/engine/test
  • go build ./pkg/frontend/databranchutils ./pkg/frontend ./pkg/sql/compile ./pkg/vm/engine/test
  • git diff upstream/main --check
  • full mo-pr-preflight-review and mo-self-review

The BVT fixtures are included for DIFF/MERGE identity redefinition and table/account/database historical-owner cleanup; they will be executed by CI because this local worktree has no standalone mo-tester runner.

@mergify

mergify Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

⚠️ The sha of the head commit of this PR conflicts with #24666. Mergify cannot evaluate rules on this PR. Once #24666 is merged or closed, Mergify will resume processing this PR. ⚠️

@matrix-meow matrix-meow added the size/XXL Denotes a PR that changes 2000+ lines label Jul 20, 2026
@VioletQwQ-0
VioletQwQ-0 marked this pull request as ready for review July 20, 2026 05:53
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

One blocking schema-evolution regression found. Focused frontend tests could not run because the worktree lacks cgo/libmo.dylib.

P1 - Allow target-only historical type changes or reject them consistently (pkg/frontend/data_branch_hashdiff.go:2625)

checkSchemaCompatibility accepts a target-only column regardless of its type, so t0(a,b) and branch t1(a,b,c varchar) are compatible. However, after t1 first adds c int and then modifies it to varchar, every DIFF/MERGE walks the old physical generation and this mapping rejects old c int against endpoint c varchar before even opening CollectChanges. The column is target-only and is deliberately omitted from MERGE apply (commonVisibleIdxes), so its historical representation cannot affect t0; nevertheless all operations fail, including when there were no writes to c. Skip/project such non-common historical columns as NULL, or make the endpoint compatibility check reject this evolution up front. Add an end-to-end regression covering add-target-only-column -> modify its type -> DIFF/MERGE.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed the blocking historical-generation schema mapping regression.

Changes:

  • Thread the endpoint target-only column indexes into historical generation mapping.
  • When a historical physical column has a different type from a target-only endpoint column, leave it unmapped so projection creates a NULL vector with the endpoint type; existing endpoint hydration restores the current value when the row still exists.
  • Keep historical type drift for common columns and primary-key incompatibility fail-closed.
  • Preserve compatible target-only mappings, source-only/rename behavior, and reordered columns.

Coverage:

  • Added focused unit coverage for the old failure, typed-NULL projection and vector ownership, common-column rejection, compatible target-only columns, source-only/rename, and reorder mappings.
  • Added DIFF BVT cases for no writes and historical target-only values across ADD c INT -> MODIFY c VARCHAR.
  • Added a MERGE BVT case proving common-column changes apply while the historical target-only value is ignored by destination apply.

Validation:

  • go test ./pkg/frontend -count=1
  • focused go test -race ./pkg/frontend ... -count=1
  • go vet ./pkg/frontend
  • go build ./pkg/frontend
  • git diff --check
  • full mo-self-review across correctness, concurrency, lifecycle, compatibility, failure modes, API closure, and Q1-Q3: zero open blockers

The BVT fixtures were not executed locally because this worktree has no mo-tester runner; the new-head CI will validate them.

@aunjgr aunjgr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed updated head c515a14. The target-only historical type-drift blocker is closed: incompatible non-common columns are projected as NULL and hydrated from the endpoint, while common-column and primary-key drift remain rejected. DIFF and MERGE regressions cover both unchanged and updated rows. No remaining blockers found.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

One blocking timestamp-source race remains despite the new current-source lock.

P1 - Serialize timestamp-based data branch creation with ALTER (pkg/frontend/clone.go:461)

The new shared source lock is skipped whenever snapshot.TS is set. That includes {MO_TS=...} / timestamp sources (ResolveTsHint constructs a Snapshot with TS), not just named snapshots. Thus a timestamp-based CREATE DATA BRANCH can clone old generation A while a concurrent COPY ALTER acquires the exclusive catalog lock, sees neither branch_metadata nor a kind='user' snapshot/PITR, and omits the A→C lineage edge. Once the create commits B→A, B and current C are disconnected. DIFF/MERGE then takes the no-LCA path, whose endpoint resolver matches only name/type; after a same-name drop/re-add, it can accept and merge B's old column values into C's unrelated replacement column. Acquire the same shared lock for timestamp sources (or otherwise atomically publish the lineage before ALTER can probe), and add a concurrent {MO_TS=...} CREATE/ALTER regression test.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed the timestamp-source publication race in 21cc25707, with the deterministic concurrency regression completed in c04f9e350.

  • Clone source classification now distinguishes durable named snapshots (ExtraInfo != nil) from timestamp hints (TS/Tenant only).
  • Live and {MO_TS=...} table sources acquire the same shared mo_tables row lock used to serialize with COPY ALTER; named snapshots retain the existing skip.
  • Database/account source-set cloning uses the same classification, and nested table clones do not reacquire locks already held in sorted order.
  • Added a deterministic shared/exclusive publication-gate regression proving a timestamp clone waits while ALTER holds the exclusive lock and resumes after release.

Validation:

  • go test ./pkg/frontend -count=1
  • focused tests and go test -race ... -count=10
  • go vet ./pkg/frontend
  • git diff --check
  • exact-head preflight PASS on c04f9e3501330b67cf419540fac210b769059f4b

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

Two blocking correctness gaps remain on c04f9e3. Prior review findings were not repeated where the current code disproves or closes them. Focused tests could not run because this environment has no Go executable.

P1 - Revalidate timestamp sources when ALTER acquires the publication lock first (pkg/frontend/clone.go:478)

The timestamp is resolved before this shared lock is acquired, and the code continues unconditionally after waiting. If COPY ALTER acquires the exclusive lock first, it can replace A with C and commit without seeing the unpublished branch; the clone then resumes and restores its already-selected historical A, publishing only B→A. No A→C lineage exists, so the successful branch later fails DIFF/MERGE as missing legacy ALTER lineage (or reaches the unrelated/no-LCA path). The new concurrency test explicitly exercises ALTER-first ordering but only asserts that the clone resumes; it does not verify generation identity or lineage closure. After acquiring the lock, reject/retry when the selected timestamp generation is no longer connected to the current source, or publish through an atomic protocol that closes A→C.

P1 - Detect same-statement DROP/ADD column redefinitions (pkg/frontend/data_branch.go:1889)

Redefinition detection only notices that a name disappears in an intermediate physical generation. MatrixOne supports multi-clause COPY ALTER, so ALTER TABLE t1 DROP COLUMN b, ADD COLUMN b INT DEFAULT 0 creates one old→new edge where both endpoint schemas contain b; this name lookup therefore treats the unrelated columns as continuous. If b was updated before the ALTER, historical mapping also maps old b into new b, allowing DIFF/MERGE to replay the stale value. Existing regressions use two separate ALTER statements and therefore do not cover this case. Preserve operation-level identity across a COPY ALTER or otherwise reject a same-name replacement even when no intermediate schema exists.

@XuPeng-SH XuPeng-SH left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for two P1 correctness gaps in schema-evolution lineage. The current CI and unit/race tests pass, but the concurrency test only checks wait/resume and the DROP/ADD test uses two separate ALTER generations, so neither failure below is covered.

Comment thread pkg/frontend/clone.go
Comment thread pkg/frontend/data_branch.go
@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed both current blocking review clusters on exact head 36723ddec0 (reported independently by gouhongshen and XuPeng-SH):

  • Timestamp/MO_TS clone publication now revalidates after acquiring the shared source-row lock: it advances the RC snapshot, re-resolves the historical and current physical generations, and requires a DAG LCA before restore or metadata publication. ALTER-first disconnected generations fail closed; connected and same-generation cases remain valid.
  • A same-statement DROP b, ADD b COPY ALTER now carries action-level DROP/ADD identity through planner deep-copy and is rejected only while data-branch lineage is active. Ordinary tables and non-replacement multi-clause ALTERs remain unchanged.

Validation on this exact head: full frontend/databranchutils/plan/compile tests; focused x10 and race x10 regressions; vet; git diff --check; exact-head preflight and complete self-review PASS. Both XuPeng-SH inline threads were answered and resolved. Re-requesting both reviewers on this head.

@XuPeng-SH XuPeng-SH left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on the latest head. The same-statement DROP+ADD replacement is now rejected for data-branch lineages, which closes the previous identity-reuse issue. However, the timestamp-race fix introduces one ordinary CLONE/restore compatibility regression and still leaves revalidation non-atomic with lineage GC. Both can affect correctness, so this is not safe to approve yet.

Comment thread pkg/frontend/clone.go Outdated
Comment thread pkg/frontend/clone.go Outdated

@aptend aptend left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Track same-statement column replacement by stable identity, not raw action names

The new guard can still be bypassed by a multi-clause COPY ALTER such as:

ALTER TABLE t
  RENAME COLUMN b TO tmp,
  DROP COLUMN tmp,
  ADD COLUMN b INT DEFAULT 0;

buildAlterTableCopy resolves the recorded drop against the original tableDef by the action name. After the rename, tmp exists only in the evolving CopyTableDef, so DROP tmp produces no DropColumn action. The detector then sees only ADD b and does not reject the replacement. The lineage walk still matches the old and new b by name, so DIFF/MERGE can project an old branch value into the semantically new column.

Please track the dropped/added column through the evolving schema using stable identity (including later renames), or validate the final old-to-new identity mapping. Add DIFF/MERGE coverage for rename -> drop -> add and drop -> add -> rename variants.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

The latest changes close the previously reported direct DROP+ADD case and post-lock generation re-resolution, but four blocking compatibility/correctness gaps remain.

P1 - Scope timestamp lineage validation to DATA BRANCH clones (pkg/frontend/clone.go:573)

This block is keyed only on the timestamp snapshot, whereas the preceding lock correctly checks dataBranchCloneLockCtxKey. It therefore also runs for ordinary CREATE TABLE ... CLONE ... {MO_TS=...} and restore-generated clones. After an ordinary copy ALTER A→C with no historical owner, no A→C branch edge is retained; a still-valid pre-ALTER timestamp resolves A while current resolves C, so normal clone/restore now fails with “generation is not connected”. Gate this revalidation on explicit data-branch mode.

P1 - Hold the validated lineage through branch publication (pkg/frontend/clone.go:169)

constructBranchDAG performs a plain SELECT and may use an independent internal executor, so the validated rows are not locked by the transaction that later publishes branch metadata. A clone can read C→A and accept A, concurrent lineage GC can delete C→A and its snapshot, and then the clone can insert B→A and its distinct snapshot without a write conflict. The committed branch B is disconnected from current C again. Lock the required lineage rows in the publication transaction or atomically register the new owner.

P1 - Detect rename-then-name-reuse replacements in one ALTER (pkg/sql/compile/alter.go:685)

The new guard intersects only explicit DropColumn and AddColumn actions. ALTER TABLE t RENAME COLUMN b TO old_b, ADD COLUMN b INT DEFAULT 0 records ADD b but no drop, so it passes with active lineage. Across the resulting lineage-only edge, dataBranchColumnReachesLCA matches the new b to ancestor b by name while treating old_b as target-only; DIFF/MERGE can therefore compare or apply the replacement column and historical projection can move old b values into it. Track rename/change identity transitions as well as drops.

P1 - Allow OUTPUT AS to materialize target-only columns (pkg/frontend/data_branch_output.go:420)

Schema reconciliation now permits target-only columns, and an unqualified DIFF includes them in its target-layout output. OUTPUT AS defaults to all target-visible columns, but createSQL maps every projected target column back to the base by ColId/Seqnum. A newly added target column has no base identity, so the otherwise valid schema-evolved DIFF fails before creating the output table. Build target-only definitions from the target schema while retaining the mapped base definition for common/renamed columns.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed the latest exact-head CRs in f47a3571a.

For aptend’s multi-clause identity case, the lineage guard no longer depends on recorded action names. It compares each original visible logical name against the final CopyTableDef using stable (ColId, Seqnum) identity. This rejects direct DROP b, ADD b, RENAME b TO tmp, DROP tmp, ADD b, and DROP b, ADD tmp, RENAME tmp TO b, while preserving ordinary MODIFY, pure RENAME/reorder, and non-replacement DROP/ADD behavior.

For XuPeng-SH’s two timestamp findings, revalidation is now limited to explicit DATA BRANCH context so normal timestamp CLONE/restore is unchanged, and accepted DAG rows are read FOR UPDATE in the same transaction that publishes branch metadata and its protect snapshot, preventing concurrent lineage GC from deleting the validated path before publication.

Validation on exact head: full frontend/databranchutils/plan/compile tests; focused race x10; planner identity matrix x10; vet; git diff --check; CR Learning Gate; exact-head preflight validator and complete self-review PASS. XuPeng-SH’s two threads were answered and are being resolved; re-requesting aptend, XuPeng-SH, and gouhongshen on this head.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Closed the remaining item from gouhongshen’s 36723ddec formal CR in b93a70ed5.

The first three findings are covered by f47a3571a (DATA BRANCH-only timestamp validation, lineage FOR UPDATE through publication, and final-schema stable identity covering rename/name-reuse). The additional OUTPUT AS blocker is now fixed as follows:

  • projected columns explicitly classified in tarOnlyIdxes use their target-side type when no base stable identity exists;
  • those output columns are nullable because base-side DIFF rows legitimately project target-only values as NULL;
  • common and renamed columns still require a stable base (ColId, Seqnum) mapping and remain fail-closed if it is missing.

Validation on exact head b93a70ed5: target-only OUTPUT AS regression x10; full frontend/databranchutils/plan/compile tests; vet; git diff --check; CR Learning Gate; refreshed exact-head preflight validator and complete self-review PASS. Re-requesting gouhongshen on the complete current head.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

Two concurrency races can leave data-branch lineage disconnected or delete history needed by a newly created PITR.

P1 - Revalidate timestamp sources after database-clone locks (pkg/frontend/clone.go:777)

Database cloning resolves its source before acquiring the sorted shared locks at lines 721-725. Nested table clones then set dataBranchCloneLockCtxKey to false here; that disables both duplicate locking and shouldRevalidateTimestampDataBranchCloneSource. If COPY ALTER publishes a new generation between source collection and lock acquisition, the nested clone still restores the old generation at the MO_TS and updateBranchMetaTable records that old ID as the parent. When no prior branch or historical owner exists, ALTER creates no connecting lineage edge, so the new branch is published against a disconnected generation and later DIFF/MERGE cannot safely establish its LCA. Revalidate the complete database source set once after all locks are held, while retaining the no-reacquire behavior for nested clones.

P1 - Synchronize PITR creation with lineage compaction (pkg/sql/compile/alter.go:367)

The compaction source query reads active PITR rows without locking them, while compaction only locks mo_branch_metadata. CREATE/activate PITR does not acquire that metadata lock, so a PITR can become active after this read but before the subsequent snapshot and metadata deletes. If its retention lower bound covers an ALTER generation, the stale plan still deletes that generation and its branch snapshot, making the newly created PITR unable to restore it. The source set must be synchronized with compaction (or revalidated while holding the deletion lock) before deleting lineage.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

已处理当前 head 7a3e913c1 上的两项 CR,推送为 c35f1cf5c

  1. 数据库级 {MO_TS=...} DATA BRANCH 在拿到全部源表共享锁后,会先推进 RC snapshot,再逐个重校验所有非 view 源表的 selected/current generation;nested table clone 仍不重复拿锁,但不再跳过整库重校验。任何断开的 generation 会在创建目标数据库或 restore 之前失败。
  2. CREATE PITR 事务现在会先获取与 lineage compaction 相同的 mo_branch_metadata FOR UPDATE 行锁,并持有到 PITR publication commit/rollback。这样 compaction 要么看到新 PITR owner,要么先完成删除后 PITR 才线性化创建,不再存在 owner read 与 delete 之间插入 PITR 的窗口。

验证:

  • go test ./pkg/frontend ./pkg/frontend/databranchutils ./pkg/sql/compile -count=1
  • 相关并发回归 go test -race ... -count=10
  • go vet ./pkg/frontend ./pkg/frontend/databranchutils ./pkg/sql/compile
  • git diff --check
  • exact-head preflight PASS:c35f1cf5c06abca8aebbc9abe90fa1df159e90f4

已重新请求所有仍 blocking 的 reviewer 一起 review 当前 head。

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

The latest database timestamp-source and PITR-publication fixes close the two re-review findings, but named-snapshot branch creation still has a blocking lineage-publication race.

P1 - Make named-snapshot lineage ownership atomic with snapshot deletion (pkg/frontend/clone.go:620)

Revalidation and the mo_branch_metadata FOR UPDATE lock only run for timestamp hints; named snapshots (ExtraInfo != nil) skip both paths. Consider snapshot S created on physical generation A, followed by a snapshot-covered COPY ALTER A→C that records lineage C→A. A DATA BRANCH clone can resolve S, then concurrent DROP SNAPSHOT can delete S and run compactHistoricalAlterLineageWithBH, removing C→A before the clone publishes its independent B→A metadata/snapshot rows. The clone uses the already-resolved timestamp, so it can still succeed, but B is now disconnected from current C and later DIFF/MERGE cannot establish the correct LCA. This affects table and database clones. Lock/revalidate the named-snapshot lineage through branch publication, or atomically transfer ownership from the snapshot to the new branch.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed the named-snapshot lineage publication race in 5dda1174540b.

The DATA BRANCH transaction now locks the selected named user-snapshot row with FOR UPDATE and revalidates its full identity (name, physical timestamp, level, object ID, and kind) before any target mutation. The lock remains owned by the same background transaction through branch metadata and protect-snapshot publication.

This gives the two race outcomes explicit semantics:

  • If branch creation acquires the lock first, DROP SNAPSHOT waits until the new lineage owner is durable.
  • If DROP SNAPSHOT (or same-name recreation) wins first, branch creation observes a missing or mismatched snapshot after the wait and returns ErrTxnNeedRetryWithDefChanged before creating the target.

The guard is restricted to DATA BRANCH named-snapshot sources. Ordinary CLONE/restore paths are unchanged, and both table-level and database-level DATA BRANCH entry points use the same lock contract.

Validation:

  • go test ./pkg/frontend -run 'Test(ShouldLockNamedDataBranchCloneSnapshot|LockNamedDataBranchCloneSnapshot)$' -count=1
  • go test -race ./pkg/frontend -run 'Test(ShouldLockNamedDataBranchCloneSnapshot|LockNamedDataBranchCloneSnapshot|TimestampDataBranch)' -count=10
  • go test ./pkg/frontend ./pkg/frontend/databranchutils ./pkg/sql/compile -count=1
  • go vet ./pkg/frontend ./pkg/frontend/databranchutils ./pkg/sql/compile
  • git diff --check
  • exact-head mo-pr-preflight-review: PASS for 5dda1174540b (4944a05d...)

Re-requesting all currently blocking reviewers together.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex automated review

Reviewed 5dda117 against origin/main and the full existing review discussion. The named-snapshot locking and revalidation fix closes the prior blocker; no new actionable regressions found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature size/XXL Denotes a PR that changes 2000+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants